#include <stx/imp/cmn.hpp>

inline namespace stx
{
std::string tos(ShellResult const& shr)
{
    auto s_status = tos(shr.status);
    auto svs_size = 4uz;

    if (!shr.cout.empty())
    {
        svs_size += shr.cout.size() * 2 - 1;
    }

    if (!shr.cerr.empty())
    {
        svs_size += shr.cerr.size() * 2 - 1;
    }

    auto svs = std::vector<std::string_view>{};
    svs.reserve(svs_size);

    insert(svs, "STATUS:"sv);
    insert(svs, s_status);
    insert(svs, "|STDOUT:"sv);

    for (auto const& s : shr.cout)
    {
        insert(svs, s);
        if (&s != &shr.cout.back())
        {
            insert(svs, _I_);
        }
    }

    insert(svs, "|STDERR:"sv);
    for (auto const& s : shr.cerr)
    {
        insert(svs, s);
        if (&s != &shr.cerr.back())
        {
            insert(svs, _I_);
        }
    }

    ASSERT(svs.size() == svs_size);
    return mcat2<std::string>(svs);
}

[[nodiscard]] std::string tos(ShellRawResult const& srr)
{
    auto s_status = tos(srr.status);
    auto svs_size = 4uz;

    if (!srr.cout.empty())
    {
        svs_size += srr.cout.size() * 2 - 1;
    }

    if (!srr.cerr.empty())
    {
        svs_size += srr.cerr.size() * 2 - 1;
    }

    auto ss  = std::vector<std::string>{};
    auto svs = std::vector<std::string_view>{};

    ss.reserve(srr.cout.size() + srr.cerr.size());
    svs.reserve(svs_size);

    insert(svs, "STATUS:"sv);
    insert(svs, s_status);
    insert(svs, "|STDOUT:"sv);

    for (auto const& chunk : srr.cout)
    {
        insert(ss, bin2hex(chunk));
        insert(svs, ss.back());
        if (&chunk != &srr.cout.back())
        {
            insert(svs, _I_);
        }
    }

    insert(svs, "|STDERR:"sv);
    FOR (i, auto const& chunk : srr.cerr)
    {
        insert(ss, bin2hex(chunk));
        insert(svs, ss.back());
        if (i + 1 < srr.cerr.size())
        {
            insert(svs, _I_);
        }
    }

    ASSERT(svs.size() == svs_size);
    return mcat2<std::string>(svs);
}

struct ChildInfo__ final
{
    ::pid_t        pid;
    FileDescriptor cin;
    FileDescriptor cout;
    FileDescriptor cerr;
};

ChildInfo__ createChildProcess__(StringView const& sv_cmd)
{
    auto s_cmd = tos(sv_cmd);
    auto ret   = ChildInfo__{};

    int pipes[3][2]{};

    if (-1 == ::pipe2(pipes[0], O_CLOEXEC | O_NONBLOCK))
    {
        RUNTIME_ERROR("Failed to ::pipe2() (1): "sv, M_SYS_ERR);
    }

    if (-1 == ::pipe2(pipes[1], O_CLOEXEC | O_NONBLOCK))
    {
        ::close(pipes[0][0]);
        ::close(pipes[0][1]);

        RUNTIME_ERROR("Failed to ::pipe2() (2): "sv, M_SYS_ERR);
    }

    if (-1 == ::pipe2(pipes[2], O_CLOEXEC | O_NONBLOCK))
    {
        ::close(pipes[0][0]);
        ::close(pipes[0][1]);
        ::close(pipes[1][0]);
        ::close(pipes[1][1]);

        RUNTIME_ERROR("Failed to ::pipe2() (3): "sv, M_SYS_ERR);
    }

    auto const child_pid = ::fork();
    if (child_pid < 0)
    {
        ::close(pipes[0][0]);
        ::close(pipes[0][1]);
        ::close(pipes[1][0]);
        ::close(pipes[1][1]);
        ::close(pipes[2][0]);
        ::close(pipes[2][1]);

        RUNTIME_ERROR("Failed to ::fork(): "sv, M_SYS_ERR);
    }

    auto const is_parent = child_pid > 0;
    if (is_parent)
    {
        ::close(pipes[0][0]);
        ::close(pipes[1][1]);
        ::close(pipes[2][1]);

        ret.pid  = child_pid;
        ret.cin  = FileDescriptor(pipes[0][1]);
        ret.cout = FileDescriptor(pipes[1][0]);
        ret.cerr = FileDescriptor(pipes[2][0]);

        return ret;
    }

    ::close(pipes[0][1]);
    ::close(pipes[1][0]);
    ::close(pipes[2][0]);

    if (-1 == ::dup2(pipes[0][0], STDIN_FILENO))
    {
        std::abort();
    }

    if (-1 == ::dup2(pipes[1][1], STDOUT_FILENO))
    {
        std::abort();
    }

    if (-1 == ::dup2(pipes[2][1], STDERR_FILENO))
    {
        std::abort();
    }

    ::execl("/bin/bash", "/bin/bash", "-c", s_cmd.data(), nullptr);
    std::exit(EXIT_SUCCESS);
}

Future<Status> shell(StringView const             sv_cmd,
                     CbOnOutputBuffer             fn_on_output,
                     Duration const               dur_timeout,
                     std::optional<ShellInputter> opt_fn_cin)
{
    auto const tp_start = SteadyClock::now();

    auto child    = createChildProcess__(sv_cmd);
    auto fn_input = ShellInputter{};
    if (opt_fn_cin)
    {
        fn_input = *opt_fn_cin;
    }
    else
    {
        fn_input = [](FileDescriptor) -> Future<Status>
        { co_return C_STATUS_SUCCESS; };
    }

    auto const sp_prom_cout = std::make_shared<Promise<Status>>();
    auto const sp_prom_cerr = std::make_shared<Promise<Status>>();
    auto const child_pid    = child.pid;

    auto atomic_kill_child = Atomic<bool>(false);
    auto fn_kill_child     = [&]
    {
        atomic_kill_child.visit(
            [child_pid](auto&& is_killed)
            {
                if (!is_killed)
                {
                    ::kill(child_pid, SIGKILL);
                    is_killed = true;
                }
            });
    };

    auto fn_read = [=](std::shared_ptr<Promise<Status>> sp_prom,
                       FileDescriptor                   fd,
                       bool const                       is_cout) -> Future<>
    {
        auto buf = Buffer(C_PAGE_SIZE);

        while (true)
        {
            auto status = co_await read(fd.fd(), buf, dur_timeout);
            if (dur_timeout > 0ms && elapsed(tp_start) >= dur_timeout)
            {
                LOG_ERROR("Shell command is timeout: "sv,
                          dur_timeout.ms.count(),
                          _I_,
                          sv_cmd);
                fn_kill_child();
                sp_prom->setValue(C_IO_TIMEOUT);
                co_return;
            }

            if (C_STATUS_SUCCESS == status)
            {
                sp_prom->setValue(C_STATUS_SUCCESS);
                co_return;
            }

            if (status > 0)
            {
                buf.resize(Number(status));
                status = co_await fn_on_output(is_cout, buf);
                if (status)
                {
                    LOG_INFO("Shell command is canceled by the caller. "sv,
                             sv_cmd);
                    fn_kill_child();
                    sp_prom->setValue(C_IO_CANCELLED);
                    co_return;
                }

                if (buf.size() < C_PAGE_SIZE)
                {
                    buf.resize(C_PAGE_SIZE);
                }

                continue;
            }

            sp_prom->setValue(static_cast<Status>(status));
            co_return;
        }
    };

    auto fut_cin  = fn_input(std::move(child.cin));
    auto fut_cout = sp_prom_cout->getFuture();
    auto fut_cerr = sp_prom_cerr->getFuture();

    fn_read(sp_prom_cout, std::move(child.cout), true);
    fn_read(sp_prom_cerr, std::move(child.cerr), false);

    auto tpl = co_await whenAll(fut_cin, fut_cout, fut_cerr);
    fut_cin  = std::move(std::get<0>(tpl));
    fut_cout = std::move(std::get<1>(tpl));
    fut_cerr = std::move(std::get<2>(tpl));

    auto child_status = 0;
    if (-1 == ::waitpid(child.pid, &child_status, 0))
    {
        co_return C_USER_ERRNO_BASE_1 + (-errno);
    }

    if (auto const cin_status = fut_cin.get())
    {
        co_return C_USER_ERRNO_BASE_2 + cin_status;
    }

    if (auto const cout_status = fut_cout.get())
    {
        co_return C_USER_ERRNO_BASE_3 + cout_status;
    }

    if (auto const cerr_status = fut_cerr.get())
    {
        co_return C_USER_ERRNO_BASE_4 + cerr_status;
    }

    /*
    WIFEXITED(status):
	------------------
        Did the process exit normally? As opposed to being signalled.

    WEXITSTATUS(status) == 0:
	-------------------------
        Did the process exit with exit code 0? i.e. "success".
	*/
    if (WIFEXITED(child_status))
    {
        co_return WEXITSTATUS(child_status);
    }

    co_return C_USER_ERRNO_BASE_5;
}

Future<Status> shell(StringView const sv_cmd,
                     CbOnOutputBuffer fn_on_output,
                     Duration const   dur_timeout)
{
    return shell(sv_cmd, std::move(fn_on_output), dur_timeout, std::nullopt);
}

Future<Status> shell(StringView const             sv_cmd,
                     CbOnOutputBuffer             fn_on_output,
                     std::optional<ShellInputter> opt_fn_cin)
{
    return shell(sv_cmd, std::move(fn_on_output), 0ms, std::move(opt_fn_cin));
}

Future<Status> shell(StringView const sv_cmd, CbOnOutputBuffer fn_on_output)
{
    return shell(sv_cmd, std::move(fn_on_output), 0ms, std::nullopt);
}

Future<Status> shell(StringView const             sv_cmd,
                     CbOnOutputString             fn_on_output,
                     Duration const               dur_timeout,
                     std::optional<ShellInputter> opt_fn_cin)
{
    auto fn_on_output_ =
        [MOVE(fn_on_output),
         s_cout_tmp = std::string{},
         s_cerr_tmp = std::string{}](bool const is_cout,
                                     Buffer&    buf) mutable -> Future<Status>
    {
        auto       i    = 0uz;
        auto const size = buf.size();

        for (; i < size; ++i)
        {
            auto c = static_cast<char>(buf[i]);
            if ('\r' == c)
            {
                continue;
            }

            if (c != '\n')
            {
                if (is_cout)
                {
                    s_cout_tmp.push_back(static_cast<char>(c));
                }
                else
                {
                    s_cerr_tmp.push_back(static_cast<char>(c));
                }

                continue;
            }

            if (is_cout)
            {
                fn_on_output(true, std::move(s_cout_tmp));
                s_cout_tmp.clear();
            }
            else
            {
                fn_on_output(false, std::move(s_cerr_tmp));
                s_cerr_tmp.clear();
            }
        }

        if (!s_cout_tmp.empty())
        {
            fn_on_output(true, std::move(s_cout_tmp));
            s_cout_tmp.clear();
        }

        if (!s_cerr_tmp.empty())
        {
            fn_on_output(false, std::move(s_cerr_tmp));
            s_cerr_tmp.clear();
        }

        co_return C_STATUS_SUCCESS;
    };

    return shell(sv_cmd, std::move(fn_on_output_), dur_timeout, opt_fn_cin);
}

Future<Status> shell(StringView const             sv_cmd,
                     CbOnOutputString             fn_on_output,
                     std::optional<ShellInputter> opt_fn_cin)
{
    return shell(sv_cmd, std::move(fn_on_output), 0ms, std::move(opt_fn_cin));
}

Future<Status> shell(StringView const sv_cmd,
                     CbOnOutputString fn_on_output,
                     Duration const   dur_timeout)
{
    return shell(sv_cmd, std::move(fn_on_output), dur_timeout, std::nullopt);
}

Future<Status> shell(StringView const sv_cmd, CbOnOutputString fn_on_output)
{
    return shell(sv_cmd, std::move(fn_on_output), 0ms, std::nullopt);
}

Future<ShellResult>
shell(StringView const sv_cmd, Duration const dur_timeout, MemView mv_cin)
{
    auto fn_inputter =
        [mv_cin, dur_timeout](FileDescriptor fd) mutable -> Future<Status>
    {
        while (!mv_cin.empty())
        {
            auto const status = co_await write(fd.fd(), mv_cin, dur_timeout);
            if (status <= 0)
            {
                LOG_ERROR("Failed to write: "sv,
                          status,
                          _I_,
                          fd.fd(),
                          _I_,
                          mv_cin.size(),
                          _I_,
                          dur_timeout.ms.count());

                co_return static_cast<Status>(status);
            }

            if (Number(status) >= mv_cin.size())
            {
                co_return C_STATUS_SUCCESS;
            }

            mv_cin =
                MemView(mv_cin.data() + status, mv_cin.size() - Number(status));
        }

        co_return C_STATUS_SUCCESS;
    };

    auto ret          = ShellResult{};
    auto fn_on_output = [&](bool const  is_cout,
                            std::string line) -> Future<Status>
    {
        insert(is_cout ? ret.cout : ret.cerr, std::move(line));
        co_return C_STATUS_SUCCESS;
    };

    ret.status = co_await shell(
        sv_cmd, std::move(fn_on_output), dur_timeout, std::move(fn_inputter));
    co_return ret;
}

Future<ShellResult> shell(StringView const sv_cmd, Duration const dur_timeout)
{
    return shell(sv_cmd, dur_timeout, {});
}

Future<ShellResult> shell(StringView const sv_cmd, MemView const mv_cin)
{
    return shell(sv_cmd, 0ms, mv_cin);
}

Future<ShellResult> shell(StringView const sv_cmd)
{
    return shell(sv_cmd, 0ms, {});
}

Future<ShellRawResult>
shellRaw(StringView const sv_cmd, Duration const dur_timeout, MemView mv_cin)
{
    auto fn_inputter =
        [mv_cin, dur_timeout](FileDescriptor fd) mutable -> Future<Status>
    {
        while (!mv_cin.empty())
        {
            auto const status = co_await write(fd.fd(), mv_cin, dur_timeout);
            if (status <= 0)
            {
                LOG_ERROR("Failed to write: "sv,
                          status,
                          _I_,
                          fd.fd(),
                          _I_,
                          mv_cin.size(),
                          _I_,
                          dur_timeout.ms.count());

                co_return static_cast<Status>(status);
            }

            if (Number(status) >= mv_cin.size())
            {
                co_return C_STATUS_SUCCESS;
            }

            mv_cin =
                MemView(mv_cin.data() + status, mv_cin.size() - Number(status));
        }

        co_return C_STATUS_SUCCESS;
    };

    auto ret          = ShellRawResult{};
    auto fn_on_output = [&](bool const is_cout, Buffer& chunk) -> Future<Status>
    {
        insert(is_cout ? ret.cout : ret.cerr, std::move(chunk));
        co_return C_STATUS_SUCCESS;
    };

    ret.status = co_await shell(
        sv_cmd, std::move(fn_on_output), dur_timeout, std::move(fn_inputter));
    co_return ret;
}

Future<ShellRawResult> shellRaw(StringView const sv_cmd,
                                Duration const   dur_timeout)
{
    return shellRaw(sv_cmd, dur_timeout, {});
}

Future<ShellRawResult> shellRaw(StringView const sv_cmd, MemView const mv_cin)
{
    return shellRaw(sv_cmd, 0ms, mv_cin);
}

Future<ShellRawResult> shellRaw(StringView const sv_cmd)
{
    return shellRaw(sv_cmd, 0ms, {});
}
} // namespace stx